package isssr.bean.entity;

import isssr.bean.exception.*;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.exception.ConstraintViolationException;

public abstract class MetaEntity implements Serializable {
	
	private static final long serialVersionUID = 1L;
	private static final SessionFactory sessionFactory  = MetaEntity.buildSessionFactory();
	private Session currentSession = null;
	
	private static SessionFactory buildSessionFactory()
	{
		if(MetaEntity.sessionFactory!=null) 
			return MetaEntity.sessionFactory;
		
        try {
        	Configuration cfg =new Configuration();
        	// Create the SessionFactory from hibernate.cfg.xml
            return cfg.configure().buildSessionFactory();
        }
        catch (Throwable ex) {
            // Make sure you log the exception, as it might be swallowed
            System.err.println("Initial SessionFactory creation failed." + ex);
            throw new ExceptionInInitializerError(ex);
        }
	}
	
	private Session openSession() throws  StorageFailureException
	{
		try
		{
			if(this.currentSession!=null && this.currentSession.isOpen())
			{
				this.closeSession();
			}
			
			this.currentSession = sessionFactory.openSession();
			if(!this.currentSession.isOpen())
				System.out.println("SESSIONE NON APERTA!");
			return this.currentSession;
		}
		catch(HibernateException ex)
		{
			StorageFailureException sf = new StorageFailureException();
			sf.initCause(ex);
			throw sf;
		}
	}
	
	
	private void closeSession() throws StorageFailureException
	{
		try
		{
			if(this.currentSession != null && this.currentSession.isOpen())
			{
				this.currentSession.close();
				this.currentSession = null;
			}
		}
		catch(HibernateException ex)
		{
			StorageFailureException sf = new StorageFailureException();
			sf.initCause(ex);
			throw sf;
		}
		
		return;
	}
	
	
	private HashMap<String, Object> getFieldList() throws IllegalAccessException, IllegalArgumentException
	{
		HashMap<String, Object> fieldList = new HashMap<String, Object>();
		Field[] fields = this.getClass().getDeclaredFields();
		
		for(Field field : fields)
		{
			if(!field.isAccessible())
			{
				field.setAccessible(true);
				fieldList.put(field.getName(), field.get(this));
				field.setAccessible(false);
			}
			else
			{
				fieldList.put(field.getName(), field.get(this));
			}
		}
		
		return fieldList;
	
	}
	
	private void copyFields(MetaEntity storageEntity) throws EntityNotLoadedException
	{
		Class<? extends MetaEntity> actualClass = this.getClass();
		Field[] fields = storageEntity.getClass().getDeclaredFields();
		
		ArrayList<Object> fieldValues = new ArrayList<Object>();
		
		try
		{
			for(Field f : fields)
			{
				if(!f.isAccessible())
				{
					f.setAccessible(true);
					f.set(actualClass.cast(this), f.get(storageEntity));
					f.setAccessible(false);
				}
				else
				{
					f.set(actualClass.cast(this), f.get(storageEntity));
				}
			}
		}
		catch(Exception ex)
		{
			EntityNotLoadedException nl = new EntityNotLoadedException();
			nl.initCause(ex);
			throw nl;
		}
		return;
	}
	
	private Object pickFromStorage() throws NoMetaEntityIDException, 
											NoEntityFoundException
	{	
		if(this.getMetaID()==null)
			throw new NoMetaEntityIDException();
		
		Object result = this.currentSession.get(this.getClass(), this.getMetaID());
		
		if(result == null) 
			throw new NoEntityFoundException(this.getClass().getName());
		
		return result; 
	}
	
	
	
	
	
	private String buildQueryString(HashMap<String,Object> fieldList)
	{
		boolean firstParameter = true;
		
		String queryString = "select o from " + this.getClass().getSimpleName() + " as o";
		
		for(Entry<String, Object> e : fieldList.entrySet())
		{
			
			if(e.getValue() != null)
			{
				if(firstParameter)
				{
					firstParameter = false;
					queryString = queryString + " where ";
				}
				else
				{
					queryString = queryString + " and ";
				}
				
				
				queryString = queryString + "o." + e.getKey() + " = :" + e.getKey();
			}
		}
		
		return queryString;
		
	}
	
	private void buildQueryParameters(HashMap<String, Object> fieldList, Query query)
	{		
		Class<? extends Object> parameterClass=null;
		Object parameterValue = null;
				
		for(Entry<String,Object> e : fieldList.entrySet())
		{
			parameterValue = e.getValue();
			
			if(parameterValue != null)
			{
				parameterClass = parameterValue.getClass();
				
				if(parameterClass.isPrimitive() 
						|| parameterValue instanceof Number 
						|| parameterValue instanceof String
						|| parameterValue instanceof Boolean)
				{
					query.setParameter( e.getKey(), parameterValue);
				}
				else
				{
					query.setEntity(e.getKey(), parameterValue);
				}
			}
		}
	}
	
	private Query buildQuery(HashMap<String,Object> fieldList)
	{
		Query query;
		
		query = currentSession.createQuery(
							this.buildQueryString(fieldList)
							);

		this.buildQueryParameters(fieldList, query);
		
		return query;
	}


	abstract protected Serializable getMetaID();
	
	public MetaEntity()
	{
		this.currentSession = null;
	}
	
	
	
	/**
	 * Inizializza i campi dell'oggetto su cui è invocato con i valori della corrispondente
	 * entità su DB. L'entità su DB viene individuata a partire dal parametro identificativo
	 * specificato dal metodo getMetaID()
	 * @throws StorageFailureException 
	 * 
	 * @throws Exception
	 */
	
	public void load() throws NoMetaEntityIDException, NoEntityFoundException, EntityNotLoadedException, StorageFailureException
	{
		if(this.getMetaID() == null)
			throw new NoMetaEntityIDException();
		
		MetaEntity entityFromStorage = null;
		Class<? extends MetaEntity> actualClassType = this.getClass();
		
		Session session = openSession();
		
		Transaction tx = session.beginTransaction();
		
		entityFromStorage = actualClassType.cast(pickFromStorage());
		
		tx.commit();
		
		if(entityFromStorage == null) 
		{	
			closeSession();
			throw new NoEntityFoundException(actualClassType.getName());
		}
		
		try
		{
			this.copyFields(entityFromStorage);
		}
		catch(EntityNotLoadedException ex)
		{
			throw ex;
		}
		finally
		{
			closeSession();	
		}
	}
	
	public void update() throws NoMetaEntityIDException, StorageFailureException, StorageConflictException 
	{
		if(this.getMetaID() == null)
			throw new NoMetaEntityIDException();

		try
		{
			Session session = this.openSession();
			
			Transaction tx = session.beginTransaction();
			
			session.update(this);
		
			tx.commit();
		}
		catch(ConstraintViolationException ex)
		{
			StorageConflictException sc = new StorageConflictException();
			sc.initCause(ex);
			throw sc;
		}
		finally
		{
			closeSession();	
		}
	}
	
	public void save() throws StorageConflictException, StorageFailureException  
	{
		
		try
		{
			Session session = this.openSession();
			
			Transaction tx = session.beginTransaction();
			
			session.persist(this);

			tx.commit();
		}
		catch(ConstraintViolationException ex)
		{
			StorageConflictException sc = new StorageConflictException();
			sc.initCause(ex);
			throw sc;
		}
		catch(HibernateException ex)
		{
			StorageFailureException sf = new StorageFailureException();
			sf.initCause(ex);
			throw sf;
		}
		finally
		{
			closeSession();
		}
	}
	
	public void unsave() throws  NoMetaEntityIDException, StorageFailureException
	{
		if(this.getMetaID() == null)
			throw new NoMetaEntityIDException();
		
		try
		{
			
			Session session = this.openSession();
			
			Transaction tx = session.beginTransaction();
			
			session.delete(this);
			
			tx.commit();
		}
		catch(HibernateException ex)
		{
			throw ex;
		}
		finally
		{
			closeSession();
		}
	}
	
	/**
	 * il metodo ritorna un iteratore di oggetti dello stesso tipo dell'oggetto su cio il 
	 * metodo è invocato. I parametri non null dell'oggetto chiamante sono usati per selezionare
	 * gli oggetti da caricare nell'iteratore, che saranno quindi gli oggetti con lo stesso 
	 * valore di parametri.
	 * 
	 * @return Iterator<?>
	 * @throws Exception
	 */
	
	public Iterator<?> loadSiblings(int pageNumber, int pageSize) throws Exception
	{
		Iterator<?> result = null;
		
		try
		{
			Query query;
			
			Session session = this.openSession();
			
			Transaction tx = session.beginTransaction();
			
			QueryBuilder builder = new QueryBuilder();
			
			query = builder.build(session, this.getClass().getSimpleName(), this.getFieldList());
			
			if(pageNumber>0)
				query = query.setFirstResult((pageNumber - 1) * pageSize);
			
			if(pageSize>0)
			{
				query = query.setMaxResults(pageSize);
			}
			
			System.out.println(query.getQueryString());
			System.out.println(query.getNamedParameters()[0]);
			result = query.list().iterator();
			tx.commit();
		}
		catch(Exception ex)
		{
			throw ex;
		}
		finally
		{
			this.closeSession();
		}
		
		return result;
	}
	

	public Iterator<?> loadSiblings(int pageSize) throws Exception
	{
		return loadSiblings(0, pageSize);
	}
	
	public Iterator<?> loadSiblings() throws Exception
	{
		return loadSiblings(0, 0);
	}
	
}
